package com.knife.poi.util;

import com.knife.common.utils.KnifeListSplitUtil;
import com.knife.poi.annotation.ExcelColumn;
import com.knife.poi.annotation.ExcelFile;
import com.knife.poi.annotation.MergeColumn;
import com.knife.easyexcel.write.helper.KnifeHttpServletResponseHelper;
import com.knife.poi.render.ValueRenderer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.springframework.http.MediaType;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author tianhao.luo@hand-china.com
 * @version 1.0
 * @date 2022年05月12日 星期四 9:00 上午
 * @description
 * @since 1.0
 */
@Slf4j
public class SimplePoiExportUtil {



    /**
     * @return
     * @description
     * @version 1.0
     * @author tianhao.luo@hand-china.com
     * @date 2022/5/12 9:01 上午
     * @since 1.0
     */
    public static <T, R> void export(String filePath,
                                     List<T> allList,
                                     Class<T> tClass,
                                     Comparator<T> comparator,
                                     KnifeListSplitUtil.ContinueField<T, R> continueField) {
        HSSFWorkbook workbook = null;
        FileOutputStream out = null;
        try {
            // 创建工作薄对象
            workbook = new HSSFWorkbook();
            ExcelFile excelFile = tClass.getAnnotation(ExcelFile.class);
            // 构造 excel
            buildExcel(workbook, excelFile,tClass, allList, comparator, continueField);
            //文档输出
            out = new FileOutputStream(filePath + excelFile.excelName() + ".xls");
            workbook.write(out);
        } catch (Exception e) {
            log.error("导出excel出现异常:{}",e.getMessage());
            e.printStackTrace();
            throw new RuntimeException("导出 excel 失败");
        } finally {
            if (Objects.nonNull(out)) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * @return
     * @description
     * @version 1.0
     * @author tianhao.luo@hand-china.com
     * @date 2022/5/12 9:01 上午
     * @since 1.0
     */
    public static <T, R> void export(HttpServletResponse httpServletResponse,
                                     List<T> allList,
                                     Class<T> tClass,
                                     Comparator<T> comparator,
                                     KnifeListSplitUtil.ContinueField<T, R> continueField) {
        HSSFWorkbook workbook = null;
        ServletOutputStream outputStream = null;
        try {
            // 创建工作薄对象
            workbook = new HSSFWorkbook();
            ExcelFile excelFile = tClass.getAnnotation(ExcelFile.class);
            // 构造 excel
            buildExcel(workbook, excelFile,tClass, allList, comparator, continueField);
            // 操作流信息
            KnifeHttpServletResponseHelper knifeHttpServletResponseHelper = new KnifeHttpServletResponseHelper(httpServletResponse);
            knifeHttpServletResponseHelper.setContentType(MediaType.ALL)
                    .setCharacterEncoding(StandardCharsets.UTF_8)
                    .setExportFileName(StandardCharsets.UTF_8, excelFile.excelName(), "xlsx");
            //文档输出
            outputStream = httpServletResponse.getOutputStream();
            workbook.write(outputStream);
        } catch (Exception e) {
            log.error("导出excel出现异常:{}",e.getMessage());
            e.printStackTrace();
            throw new RuntimeException("导出 excel 失败");
        } finally {
            if (Objects.nonNull(outputStream)) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    private static <T, R> void buildExcel(HSSFWorkbook workbook,
                                          ExcelFile excelFile,
                                          Class<T> tClass,
                                          List<T> allList,
                                          Comparator<T> comparator,
                                          KnifeListSplitUtil.ContinueField<T, R> continueField) throws Exception {
        // 合并单元格
        Field[] excelColumnFields = FieldUtils.getFieldsWithAnnotation(tClass, ExcelColumn.class);
        Field[] mergeColumnFields = FieldUtils.getFieldsWithAnnotation(tClass, MergeColumn.class);

        // 根据 sheet 页容量,切分 sheet 页
        List<List<T>> dataList = KnifeListSplitUtil.splitList(excelFile.sheetCapacity(), allList, comparator, continueField);
        Map<Integer, HSSFCellStyle> dataStyleMap = new HashMap<>(excelColumnFields.length+1);
        Map<Integer, HSSFCellStyle> headerStyleMap = new HashMap<>(excelColumnFields.length+1);
        for (int m = 0; m < dataList.size(); m++) {
            // 处理每一个 sheet 页
            HSSFSheet sheet = workbook.createSheet();
            //设置sheet的Name
            if (dataList.size() > 1) {
                workbook.setSheetName(m, excelFile.sheetName() + (m + 1));
            } else {
                workbook.setSheetName(m, excelFile.sheetName());
            }
            // 创建 excel 头信息
            createTitle(sheet, workbook, excelFile, excelColumnFields,headerStyleMap);
            log.debug("------excel header fill success-------");
            if (CollectionUtils.isNotEmpty(dataList.get(m))){
                // 填充数据
                createAllRow(dataList.get(m), sheet, workbook, excelFile, excelColumnFields,dataStyleMap);
                log.debug("------excel row fill success-------");
                // 处理序列号单元格
                if (excelFile.serialNumEnable()) {
                    fillSerialNum(dataList.get(m), sheet, excelFile);
                }
                if (Objects.nonNull(mergeColumnFields)){
                    // 处理合并单元格,合并列
                    mergeColumn(dataList.get(m), sheet, excelColumnFields, mergeColumnFields);
                    log.debug("------excel merge fill success-------");
                }
            }
        }
    }

    private static <T> void mergeColumn(List<T> sheetDataList, HSSFSheet sheet, Field[] excelColumnFields, Field[] mergeColumnFields) throws Exception {
        for (Field field : mergeColumnFields) {
            // 第一个要合并的列
            MergeColumn annotation = field.getAnnotation(MergeColumn.class);
            // 合并的依据
            String[] groupBy = annotation.groupBy();
            // 当前需要合并的列的索引
            int mergeColumnIndex = 0;
            for (int j = 0; j < excelColumnFields.length; j++) {
                if (excelColumnFields[j].getName().equals(field.getName())) {
                    mergeColumnIndex = j;
                    break;
                }
            }
            // -1 表示没有找到需要合并的行
            int end = -1;
            // 获取需要合并的行起始位置和结束位置
            for (int k = 0; k < sheetDataList.size() - 1; k++) {
                // 当前遍历的数据索引小于上一次合并结束的数据索引,则需要跳过其中的数据和结束的数据索引
                if (k <= end) {
                    continue;
                }
                // 当前行的值
                T curr = sheetDataList.get(k);
                for (int n = k + 1; n < sheetDataList.size(); n++) {
                    // 数据向下继续查找
                    T next = sheetDataList.get(n);
                    // 如果当前行数据和下一行数据的分组数据一致,则继续向下遍历下一条数据,否则需要跳出本次数据循环
                    if (groupByFieldEquals(curr, next, groupBy)) {
                        end = n;
                    } else {
                        break;
                    }
                }

                if (end != -1) {
                    HSSFRow tempRow = sheet.getRow(k + 1);
                    HSSFCell tempRowCell = tempRow.getCell(mergeColumnIndex + 1);
                    // 需要合并单元格了
                    if (k < end) {
                        CellRangeAddress region = new CellRangeAddress(k + 1, end + 1, mergeColumnIndex + 1, mergeColumnIndex + 1);
                        // 获取合并后的内容
                        tempRowCell.setCellValue(Optional.ofNullable(FieldUtils.readField(field, curr, true)).map(Object::toString).orElse(""));
                        sheet.addMergedRegion(region);
                    }
                    // 不需要合并单元格时不需要处理,因为第一次填充了数据,注意和序列号填充不一样
                }
            }

        }
    }

    private static <T> void fillSerialNum(List<T> sheetDataList, HSSFSheet sheet, ExcelFile excelFile) throws Exception {
        int serialNum = 0;
        String[] serialNumGenerate = excelFile.serialNumGenerate();
        // 合并结束的行数据索引位置,-1 表示没有找到需要合并的行
        int tempEnd = -1;
        // 获取需要合并的行起始位置和结束位置
        for (int k = 0; k < sheetDataList.size() - 1; k++) {
            // 当前遍历的数据索引小于上一次合并结束的数据索引,则需要跳过其中的数据和结束的数据索引
            if (k <= tempEnd) {
                continue;
            }
            serialNum++;
            // 当前行的值
            T curr = sheetDataList.get(k);
            for (int n = k + 1; n < sheetDataList.size(); n++) {
                // 数据向下继续查找
                T next = sheetDataList.get(n);
                // 如果当前行数据和下一行数据的分组数据一致,则继续向下遍历下一条数据,否则需要跳出本次数据循环
                if (groupByFieldEquals(curr, next, serialNumGenerate)) {
                    tempEnd = n;
                } else {
                    // 如果n == k+1 则表示这两行不同,则上一行需要填充序列号
                    HSSFRow tempRow = sheet.getRow(k + 1);
                    HSSFCell tempRowCell = tempRow.getCell(0);
                    tempRowCell.setCellValue(serialNum);
                    break;
                }
            }

            if (tempEnd != -1) {
                HSSFRow tempRow = sheet.getRow(k + 1);
                HSSFCell tempRowCell = tempRow.getCell(0);
                // 需要合并单元格了
                tempRowCell.setCellValue(serialNum);
                if (k < tempEnd) {
                    // 获取合并后的内容
                    CellRangeAddress region = new CellRangeAddress(k + 1, tempEnd + 1, 0, 0);
                    sheet.addMergedRegion(region);
                }
            }
        }
        // 最后一行数据没有填充序列号的情况如何处理?
        if (tempEnd != sheetDataList.size() - 1) {
            HSSFRow tempRow = sheet.getRow(sheetDataList.size());
            HSSFCell tempRowCell = tempRow.getCell(0);
            // 获取合并后的内容
            tempRowCell.setCellValue(++serialNum);
        }
    }


    private static <T> void createAllRow(List<T> sheetDataList, HSSFSheet sheet, HSSFWorkbook workbook, ExcelFile excelFile, Field[] excelColumnFields, Map<Integer, HSSFCellStyle> cellStyleMap) throws Exception {
        // 构造所有行信息
        // 从标题后的行开始填充数据
        for (int i = 1; i <= sheetDataList.size(); i++) {
            HSSFRow tempRow = sheet.createRow(i);
            if (excelFile.serialNumEnable()) {
                HSSFCell cell = tempRow.createCell(0);
                HSSFCellStyle style = cellStyleMap.computeIfAbsent(0, k -> createStyle(workbook, null, null));
                cell.setCellStyle(style);
                // 序列号默认填充空
                cell.setCellValue("");
            }
            for (int j = 0; j < excelColumnFields.length; j++) {
                ExcelColumn excelColumn = excelColumnFields[j].getAnnotation(ExcelColumn.class);
                Object readField = FieldUtils.readField(excelColumnFields[j], sheetDataList.get(i - 1), true);
                Object objToString = rendererObj(excelColumn, readField, sheetDataList.get(i - 1));
                String data = Optional.ofNullable(objToString)
                        .map(Object::toString)
                        .orElse("");
                createExcelColumnData(excelFile.serialNumEnable() ? j + 1 : j, tempRow, workbook, excelColumn, data,cellStyleMap);
            }
        }
    }

    private static Object rendererObj(ExcelColumn excelColumn, Object source, Object rowData) throws Exception {
        if (Objects.isNull(source)) {
            return null;
        }
        // 此时只用第一个 render 来渲染
        Class<? extends ValueRenderer> rendererClass = excelColumn.renderer();
        ValueRenderer valueRenderer = rendererClass.newInstance();
        return valueRenderer.render(source, rowData, excelColumn.pattern());
    }

    private static void createExcelColumnData(int index, HSSFRow row, HSSFWorkbook workbook, ExcelColumn excelColumn, String data, Map<Integer, HSSFCellStyle> cellStyleMap) {
        HSSFCell rowCell = row.createCell(index);
        HSSFCellStyle excelColumnStyle = cellStyleMap.computeIfAbsent(index, k -> createStyle(workbook, excelColumn.horizontalAlignment(), excelColumn.verticalAlignment()));
        rowCell.setCellStyle(excelColumnStyle);
        rowCell.setCellValue(data);
    }


    private static void createTitle(HSSFSheet sheet, HSSFWorkbook workbook, ExcelFile excelFile, Field[] excelColumnFields, Map<Integer, HSSFCellStyle> headerStyleMap) {
        HSSFRow row = sheet.createRow(0);
        // 如果需要生成序列号,则先插入序列号列进去
        if (excelFile.serialNumEnable()) {
            // 创建序列号字段
            createSerialNumColumn(row, workbook, excelFile,headerStyleMap);
        }
        for (int i = 0; i < excelColumnFields.length; i++) {
            ExcelColumn excelColumn = excelColumnFields[i].getAnnotation(ExcelColumn.class);
            createExcelColumnTitle(excelFile.serialNumEnable() ? i + 1 : i, row, workbook, excelColumn,headerStyleMap);
            // 设置列宽度
            sheet.setColumnWidth(excelFile.serialNumEnable() ? i + 1 : i, excelColumn.width() * 256);
        }
    }

    private static void createExcelColumnTitle(int index, HSSFRow row, HSSFWorkbook workbook, ExcelColumn excelColumn, Map<Integer, HSSFCellStyle> headerStyleMap) {
        HSSFCell rowCell = row.createCell(index);
        HSSFCellStyle hssfCellStyle = headerStyleMap.computeIfAbsent(index, k -> {
            // 设置标题的格式
            HSSFCellStyle excelColumnStyle = createStyle(workbook, excelColumn.horizontalAlignment(), excelColumn.verticalAlignment());
            excelColumnStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            excelColumnStyle.setFillBackgroundColor(excelColumn.titleBackGround().getIndex());
            excelColumnStyle.setFillForegroundColor(excelColumn.titleBackGround().getIndex());
            return excelColumnStyle;
        });
        rowCell.setCellStyle(hssfCellStyle);
        rowCell.setCellValue(excelColumn.name());
    }

    private static void createSerialNumColumn(HSSFRow row, HSSFWorkbook workbook, ExcelFile excelFile, Map<Integer, HSSFCellStyle> headerStyleMap) {
        // 创建序列号字段
        HSSFCell cell = row.createCell(0);
        HSSFCellStyle hssfCellStyle = headerStyleMap.computeIfAbsent(0, k -> {
            // 设置标题的格式
            HSSFCellStyle serialNumStyle = createStyle(workbook, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
            serialNumStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            serialNumStyle.setFillBackgroundColor(HSSFColor.HSSFColorPredefined.PALE_BLUE.getIndex());
            serialNumStyle.setFillForegroundColor(HSSFColor.HSSFColorPredefined.PALE_BLUE.getIndex());
            return serialNumStyle;
        });
        cell.setCellValue(excelFile.serialNumColumnName());
        cell.setCellStyle(hssfCellStyle);
    }

    private static HSSFCellStyle createStyle(HSSFWorkbook workbook, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) {
        HSSFCellStyle style = workbook.createCellStyle();
        style.setAlignment(horizontalAlignment == null ? HorizontalAlignment.CENTER : horizontalAlignment);
        style.setVerticalAlignment(verticalAlignment == null ? VerticalAlignment.CENTER : verticalAlignment);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        return style;
    }


    private static <T> boolean groupByFieldEquals(T curr, T next, String[] groupBy) throws Exception {
        boolean groupByFieldMatchFlag = true;
        for (String fieldName : groupBy) {
            Object currField = FieldUtils.readField(curr, fieldName, true);
            Object nextField = FieldUtils.readField(next, fieldName, true);
            if (!Objects.equals(currField, nextField)) {
                groupByFieldMatchFlag = false;
                break;
            }
        }
        return groupByFieldMatchFlag;
    }
}
